Equipe 01_Systèmes embarqués_Livrable-03

Projet : Worldwide Weather Watcher – Station météo embarquée

Équipe 01 : ALLOUCH Bilal (Chef d’équipe) / COLSON Paul / LECOQ Aliça / LINARD Raphaël

Introduction

L’Agence Internationale pour la Vigilance Météorologique (AIVM) lance un programme international pour équiper les navires de stations météo embarquées capables de mesurer les paramètres influençant la formation de cyclones et autres catastrophes naturelles.

Notre startup, OceanTech Systems, a été choisie pour concevoir le prototype de cette station.
L’objectif est de créer un système simple, fiable et autonome, pouvant être piloté par n’importe quel membre d’équipage, sans connaissances techniques.

Cette station permet de mesurer la température, l’humidité, la pression et la luminosité, d’enregistrer les données sur carte SD avec date et heure, et d’indiquer l’état du système par LED.
Elle contribue à la prévention des risques météorologiques en mer grâce à une surveillance continue et automatisée.

Partie 1 — Documentation Utilisateur : Guide d’utilisation de la station météo

Destinée aux utilisateurs non techniciens (membres d’équipage, opérateurs).
Cette partie explique comment utiliser la station météo, à quoi servent les composants, comment la faire fonctionner, et comment interpréter les voyants et les données enregistrées.

Présentation des composants de la station météo

Dans cette section, vous trouverez les photos et descriptions détaillées de chaque élément utilisé dans la station météo.




Vidéo de montage et connexion des composants

Description de la vidéo :

Cette vidéo présente étape par étape le montage complet de la station météo embarquée réalisée sur Arduino UNO.
Elle montre comment connecter correctement chaque module au shield Grove et à la carte SD, afin d’obtenir un système fonctionnel prêt à exécuter le programme de la station.

Ordre des connexions dans la vidéo :

1 Installation de la carte Arduino UNO sur la table de montage.
2 Ajout de la carte Grove Base Shield sur l’Arduino (interface principale).
3 Branchement du module double bouton (rouge/vert) sur le port D2 du shield.
4 Connexion du capteur de luminosité sur l’entrée A0 du shield.
5 Raccordement de la LED RGB chaînable sur la sortie D6.
6 Connexion du capteur BME280 sur le bus I²C du shield (SDA/SCL).
7 Insertion du module carte SD Shield sur l’Arduino, puis de la carte microSD dans le lecteur.
8 Branchement du module RTC DS1307 sur le port I²C du SD Shield.

Ce câblage assure une communication correcte entre tous les modules :

Comment utiliser la station météo embarquée

Cette partie t’explique simplement comment utiliser la station, changer les modes, et comprendre les couleurs de la LED.
Tu n’as pas besoin de savoir programmer : il suffit de suivre les étapes.


1. Démarrage du système

Étape Action Résultat
1 Branche la station météo à son alimentation 5V (USB ou batterie).
2 La LED s’allume verte fixe 🟢 La station démarre correctement en mode Standard.
3 La station commence à mesurer automatiquement : température, humidité, pression atmosphérique, et luminosité ambiante.
4 Toutes les mesures sont enregistrées sur la carte microSD, avec la date et l’heure. Les fichiers contiennent une ligne par mesure, lisible sur un ordinateur.

En cas de coupure d’alimentation, la station redémarre automatiquement en mode Standard.


2. Les différents modes de fonctionnement

La station dispose de 4 modes principaux.
Tu peux changer de mode grâce aux deux boutons (un vert et un rouge) selon la durée d’appui indiquée.

Mode Comment y accéder Durée d’appui LED Description
🟢 Mode Standard Démarrage normal (aucun bouton pressé) 🟢 Vert fixe Mode principal : la station fait des mesures toutes les 10 s et les enregistre sur la carte SD.
🔵 Mode Économique Depuis le mode Standard → appui long sur le bouton vert ≈ 5 s 🔵 Bleu fixe Mesures toutes les 20 s pour économiser la batterie. Pour revenir au mode Standard, fais un appui long sur le bouton vert.
🟠 Mode Maintenance Depuis le mode Standard ou Éco → appui long sur le bouton rouge ≈ 5 s 🟠 Orange fixe Permet de retirer la carte SD en sécurité et de consulter les mesures depuis un PC. Appui long sur le bouton rouge pour quitter.
🟡 Mode Configuration Maintiens le bouton rouge pendant la mise sous tension 🟡 Jaune fixe Configure les paramètres (intervalle, taille de fichier, date/heure…). Si aucun réglage n’est fait, retour automatique au mode Standard.

Chaque mode est clairement visible grâce à la couleur fixe de la LED.


3. Comprendre les signaux lumineux de la LED

La LED indique l’état du système ou une erreur éventuelle.

Couleurs fixes (état normal)
Couleur Signification
🟢 Vert fixe Mode Standard actif (mesure normale).
🔵 Bleu fixe Mode Économique (mesures plus espacées).
🟠 Orange fixe Mode Maintenance (sécurisation ou lecture SD).
🟡 Jaune fixe Mode Configuration (réglages depuis PC).
Clignotements (signal d’erreur)
Couleur alternée Signification du problème Ce qu’il faut faire
🔴↔🔵 Rouge / Bleu Erreur d’accès à l’horloge RTC (heure non disponible). Vérifie la pile du module RTC ou son branchement.
🔴↔🟢 Rouge / Vert Erreur d’accès à un capteur (ex : capteur BME280 déconnecté). Vérifie le câble du capteur.
🔴 court / 🟢 long Données incohérentes reçues d’un capteur. Vérifie la propreté du capteur ou les conditions autour.
🔴↔⚪ Rouge / Blanc Carte SD pleine. Remplace ou vide la carte microSD.
🔴 court / ⚪ long Erreur d’accès ou d’écriture sur la carte SD. Vérifie que la carte SD est bien insérée ou reformate-la en FAT32.

La LED clignote toujours en rouge avec une autre couleur pour signaler une anomalie.


4. Conseils d’utilisation


5. En résumé

Action Bouton / Durée Résultat
Allumer la station Démarrage en 🟢 Mode Standard
Passer en Mode Éco 🟢 Bouton vert – appui long (5 s) 🔵 Mode Économique
Revenir au Mode Standard 🟢 Bouton vert – appui long (5 s) 🟢 Mode Standard
Passer en Maintenance 🔴 Bouton rouge – appui long (5 s) 🟠 Mode Maintenance
Quitter Maintenance 🔴 Bouton rouge – appui long (5 s) Retour au mode précédent
Démarrer en Configuration 🔴 Bouton rouge maintenu pendant la mise sous tension 🟡 Mode Configuration
Inactivité prolongée Retour automatique au Mode Standard

Partie 2 — Documentation Technique : Analyse et conception du système embarqué

Schéma de branchements des différents composants

Code complet

#define PRETTY_UI 1 // ← passe à 0 si la taille dépasse la mémoire UNO #include <Wire.h> // i2c ---> Capteur d'humidité, température #include <SPI.h> #include <SD.h> #include <EEPROM.h> #include <RTClib.h> #include <Adafruit_BME280.h> #include <ChainableLED.h> /* ---------- Pins ---------- */ #define PIN_LED_DATA 6 #define PIN_LED_CLK 7 #define PIN_BTN_V 3 // Vert #define PIN_BTN_R 2 // Rouge #define PIN_LUM A0 #define PIN_SD_CS 4 /* ---------- Types ---------- */ enum LedColor { LED_OFF, LED_G, LED_B, LED_Y, LED_O, LED_R, LED_W }; enum Mode { STANDARD, ECONOMIQUE, MAINTENANCE, CONFIGURATION }; /* ---------- Config & capteurs ---------- */ struct Settings { uint16_t logMin; uint16_t toutSec; uint32_t maxBytes; uint8_t enMask; uint16_t crc; }; struct Sensor; /* ---------- Prototypes ---------- */ uint16_t crc16(const Settings &s); void setDefaults(void); void saveEEPROM(void); void loadEEPROM(void); void setLed(enum LedColor c); void isrV(void); void isrR(void); bool initBME(struct Sensor *s); void readBME(struct Sensor *s, float *t, float *h, float *p); bool initLUM(struct Sensor *s); void readLUM(struct Sensor *s, float *t, float *h, float *p); void logOnce(void); void enterMode(enum Mode m); void tickStd(void); void tickEco(void); void tickMaint(void); void tickCfg(void); bool openNewLog(void); void rotateIfNeed(void); void handleCmdChar(char c); void handleCmdLine(void); void printDateTime(Print &out); void printNum(Print &out, float v, uint8_t prec); void labelVal( Print &out, const __FlashStringHelper *label, float v, uint8_t prec, const __FlashStringHelper *unit ); void showHelp(void); /* ---------- Alertes LED ---------- */ enum ErrFlags { ERR_NONE = 0, ERR_RTC = 1 << 0, ERR_SENSOR = 1 << 2, ERR_INCOH = 1 << 3, ERR_SD_FULL= 1 << 4, ERR_SD_RW = 1 << 5 }; volatile uint8_t gErrors = ERR_NONE; void updateErrorLED(void); /* ---------- Globals ---------- */ ChainableLED led(PIN_LED_DATA, PIN_LED_CLK, 1); RTC_DS1307 rtc; Adafruit_BME280 bme; volatile bool fEco = false; volatile bool fMaint = false; volatile bool fStd = false; volatile bool fQuitM = false; enum Mode modeCur = STANDARD; enum Mode modePrev = STANDARD; const unsigned long HOLD_MS = 2000; volatile unsigned long tV = 0; volatile unsigned long tR = 0; bool haveRTC = false; bool haveSD = false; bool enBME = true; bool enLUM = true; Settings cfg; const uint16_t DEF_LOG_MIN = 10; // secondes const uint16_t DEF_TOUT = 3; // secondes const uint32_t DEF_MAXB = 2048; File logFile; char logName[16] = "LOG000.CSV"; uint16_t logIdx = 0; /* ---------- Sensor struct ---------- */ struct Sensor { const char *name; bool *en; bool present; bool (*initFn)(struct Sensor *); void (*readFn)(struct Sensor *, float *, float *, float *); }; Sensor sBME = { "BME", &enBME, false, initBME, readBME }; Sensor sLUM = { "LUM", &enLUM, true, initLUM, readLUM }; Sensor *sInit[] = { &sBME, &sLUM }; /* ---------- LED base + moteur ---------- */ LedColor baseColor = LED_W; unsigned long ledBeatMs = 0; bool ledPhase = false; void setLed(enum LedColor c) { switch (c) { case LED_G: led.setColorRGB(0, 0, 255, 0); break; case LED_B: led.setColorRGB(0, 0, 0, 255); break; case LED_Y: led.setColorRGB(0, 255, 255, 0); break; case LED_O: led.setColorRGB(0, 255, 100, 0); break; case LED_R: led.setColorRGB(0, 255, 0, 0); break; case LED_W: led.setColorRGB(0, 255, 255, 255); break; default: led.setColorRGB(0, 0, 0, 0); break; } } /* ---------- EEPROM ---------- */ uint16_t crc16(const Settings &s) { const uint8_t *p = (const uint8_t *)&s; uint16_t sum = 0; for (size_t i = 0; i < sizeof(Settings) - 2; i++) { sum += p[i]; } return sum ^ 0xA55A; } void setDefaults(void) { cfg.logMin = DEF_LOG_MIN; cfg.toutSec = DEF_TOUT; cfg.maxBytes = DEF_MAXB; cfg.enMask = 0; if (enBME) { cfg.enMask |= 1; } if (enLUM) { cfg.enMask |= 2; } cfg.crc = crc16(cfg); } void saveEEPROM(void) { cfg.enMask = 0; if (enBME) { cfg.enMask |= 1; } if (enLUM) { cfg.enMask |= 2; } cfg.crc = crc16(cfg); EEPROM.put(0, cfg); } void loadEEPROM(void) { EEPROM.get(0, cfg); if (crc16(cfg) != cfg.crc) { setDefaults(); saveEEPROM(); } enBME = cfg.enMask & 1; enLUM = cfg.enMask & 2; } /* ---------- ISR (boutons inversés) ---------- */ void isrV(void) { int lv = digitalRead(PIN_BTN_V); if (lv == LOW) { tV = millis(); } else { if (tV && (millis() - tV >= HOLD_MS)) { if (modeCur == ECONOMIQUE) { fStd = true; } else { fEco = true; } } tV = 0; } } void isrR(void) { int lr = digitalRead(PIN_BTN_R); if (lr == LOW) { tR = millis(); } else { if (tR && (millis() - tR >= HOLD_MS)) { if (modeCur == MAINTENANCE) { fQuitM = true; } else { fMaint = true; } } tR = 0; } } /* ---------- SD ---------- */ bool openNewLog(void) { if (logFile) { logFile.flush(); logFile.close(); } snprintf(logName, sizeof(logName), "LOG%03u.CSV", logIdx++); logFile = SD.open(logName, FILE_WRITE); if (!logFile) { gErrors |= ERR_SD_FULL; #if PRETTY_UI Serial.println(F("🟥📁 SD: ouverture fichier échouée (pleine ?")); #else Serial.println(F("SD open fail")); #endif return false; } gErrors &= ~ERR_SD_FULL; if (logFile.size() == 0) { logFile.println(F("DateTime;TempC;Hum%;hPa;Lum")); } #if PRETTY_UI Serial.print(F("📄 Nouveau log: ")); Serial.println(logName); #endif return true; } void rotateIfNeed(void) { if (logFile && (uint32_t)logFile.size() >= cfg.maxBytes) { openNewLog(); } } /* ---------- Capteurs ---------- */ bool initBME(struct Sensor *s) { bool ok = bme.begin(0x76); if (!ok) { ok = bme.begin(0x77); } s->present = ok; #if PRETTY_UI Serial.println( ok ? F("✅ BME280 détecté") : F("❌ BME280 absent") ); #else Serial.println( ok ? F("BME ok") : F("BME abs") ); #endif return ok; } bool initLUM(struct Sensor *s) { pinMode(PIN_LUM, INPUT); s->present = true; #if PRETTY_UI Serial.println(F("✅ Capteur luminosité prêt")); #else Serial.println(F("LUM ok")); #endif return true; } void readBME(struct Sensor *s, float *t, float *h, float *p) { if (!s->present || !(*s->en)) { *t = *h = *p = NAN; return; } *t = bme.readTemperature(); *h = bme.readHumidity(); *p = bme.readPressure() / 100.0f; } void readLUM(struct Sensor *s, float *t, float *h, float *p) { (void)t; (void)h; if (!(*s->en)) { *p = NAN; return; } *p = (float)analogRead(PIN_LUM); } /* ---------- Impression ---------- */ void printDateTime(Print &out) { if (!haveRTC) { out.print(F("NA")); return; } DateTime n = rtc.now(); char b[20]; snprintf( b, sizeof(b), "%04u-%02u-%02u %02u:%02u:%02u", n.year(), n.month(), n.day(), n.hour(), n.minute(), n.second() ); out.print(b); } void printNum(Print &out, float v, uint8_t prec) { if (isnan(v)) { out.print(F("NA")); return; } char buf[16]; dtostrf(v, 0, prec, buf); out.print(buf); } void labelVal( Print &out, const __FlashStringHelper *label, float v, uint8_t prec, const __FlashStringHelper *unit ) { out.print(label); out.print('='); printNum(out, v, prec); if (unit) { out.print(' '); out.print(unit); } } /* ---------- Log d’une ligne ---------- */ void logOnce(void) { float t = NAN; float h = NAN; float p = NAN; float l = NAN; sBME.readFn(&sBME, &t, &h, &p); sLUM.readFn(&sLUM, NULL, NULL, &l); if (enBME && !sBME.present) { gErrors |= ERR_SENSOR; } // Incohérences simples (hors plage) bool incoh = false; if (!isnan(t) && (t < -40.0 || t > 85.0)) { incoh = true; } if (!isnan(h) && (h < 0.0 || h > 100.0)) { incoh = true; } if (!isnan(p) && (p < 300.0 || p > 1100.0)) { incoh = true; } if (!isnan(l) && (l < 0.0 || l > 1023.0)) { incoh = true; } if (incoh) { gErrors |= ERR_INCOH; } else { gErrors &= ~ERR_INCOH; } #if PRETTY_UI Serial.print(F("⏱ ")); printDateTime(Serial); Serial.print(F(" | ")); labelVal(Serial, F("🌡️ Temp"), t, 1, F("°C")); Serial.print(F(" | ")); labelVal(Serial, F("💧 Hum"), h, 0, F("%")); Serial.print(F(" | ")); labelVal(Serial, F("🧭 Press"), p, 1, F("hPa")); Serial.print(F(" | ")); labelVal(Serial, F("💡 Lum"), l, 0, F("")); Serial.println(); #else Serial.print(F("[MEAS] ")); printDateTime(Serial); Serial.print(F(" | T=")); printNum(Serial, t, 1); Serial.print(F("C")); Serial.print(F(" | H=")); printNum(Serial, h, 0); Serial.print(F("%")); Serial.print(F(" | P=")); printNum(Serial, p, 1); Serial.print(F("hPa")); Serial.print(F(" | L=")); printNum(Serial, l, 0); Serial.println(); #endif if (haveSD) { if (logFile) { printDateTime(logFile); logFile.print(';'); printNum(logFile, t, 1); logFile.print(';'); printNum(logFile, h, 0); logFile.print(';'); printNum(logFile, p, 1); logFile.print(';'); printNum(logFile, l, 0); logFile.println(); logFile.flush(); rotateIfNeed(); gErrors &= ~ERR_SD_RW; } else { gErrors |= ERR_SD_RW; } } else { gErrors |= ERR_SD_RW; } } /* ---------- Modes ---------- */ void enterMode(enum Mode m) { modeCur = m; switch (m) { case STANDARD: baseColor = LED_G; #if PRETTY_UI Serial.println(F("🟢 Mode STANDARD — mesure toutes 10 s")); #else Serial.println(F("[STD] 10s")); #endif break; case ECONOMIQUE: baseColor = LED_B; #if PRETTY_UI Serial.println(F("🔵 Mode ÉCONOMIQUE — mesure toutes 20 s")); #else Serial.println(F("[ECO] 20s")); #endif break; case MAINTENANCE: baseColor = LED_O; #if PRETTY_UI Serial.println(F("🟠 Mode MAINTENANCE — logging désactivé. Tapez READ / EJECT")); #else Serial.println(F("[MAINT]")); #endif break; case CONFIGURATION: baseColor = LED_Y; #if PRETTY_UI Serial.println(F("🟡 Mode CONFIGURATION — tapez HELP pour l’aide.")); #else Serial.println(F("[CFG]")); #endif break; } setLed(baseColor); } unsigned long intStd(void) { // 10 s return (unsigned long)cfg.logMin * 1000UL; } unsigned long intEco(void) { // 20 s return (unsigned long)cfg.logMin * 2000UL; } void tickStd(void) { static unsigned long t = 0; unsigned long n = millis(); if (t == 0 || n - t >= intStd()) { t = n; logOnce(); } } void tickEco(void) { static unsigned long t = 0; unsigned long n = millis(); if (t == 0 || n - t >= intEco()) { t = n; logOnce(); } } void tickMaint(void) { /* silencieux */ } void tickCfg(void) { /* silencieux aussi */ } /* ---------- Aide (config claire) ---------- */ void showHelp(void) { #if PRETTY_UI Serial.println(F("\n===== AIDE CONFIG =====")); Serial.println(F(" LOG_INTERVAL=<sec> ex: LOG_INTERVAL=10")); Serial.println(F(" TIMEOUT=<s> ex: TIMEOUT=3")); Serial.println(F(" FILE_MAX_SIZE=<octets> ex: FILE_MAX_SIZE=4096")); Serial.println(F(" CAPTEUR=BME:on|off ex: CAPTEUR=BME:on")); Serial.println(F(" CAPTEUR=LUM:on|off ex: CAPTEUR=LUM:off")); Serial.println(F(" DATE=YYYY-MM-DD HH:MM:SS ex: DATE=2025-10-28 14:20:00")); Serial.println(F(" READ → mesure immédiate")); Serial.println(F(" EJECT → sécuriser retrait SD")); Serial.println(F(" RESET → paramètres par défaut")); Serial.println(F("=======================\n")); #else Serial.println( F("CMDS: LOG_INTERVAL=<s>, TIMEOUT=<s>, FILE_MAX_SIZE=<o>, CAPTEUR=BME:on|off, CAPTEUR=LUM:on|off, DATE=YYYY-MM-DD HH:MM:SS, READ, EJECT, RESET") ); #endif } /* ---------- Commandes ---------- */ char cmdBuf[96]; uint8_t cmdLen = 0; void handleCmdChar(char c) { if (c == '\r') { return; } if (c == '\n') { handleCmdLine(); cmdLen = 0; cmdBuf[0]= 0; return; } if (cmdLen < sizeof(cmdBuf) - 1) { cmdBuf[cmdLen++] = c; cmdBuf[cmdLen] = 0; } } bool starts(const char *s, const char *p) { while (*p) { if (*s++ != *p++) { return false; } } return true; } void handleCmdLine(void) { char *s = cmdBuf; while (*s == ' ') { s++; } if (*s == 0) { return; } if (strcmp(s, "HELP") == 0) { showHelp(); return; } if (strcmp(s, "VERSION") == 0) { Serial.println(F("WWW-Pretty 1.0")); return; } if (starts(s, "LOG_INTERVAL=")) { cfg.logMin = (uint16_t)atoi(s + 13); saveEEPROM(); Serial.println(F("OK LOG_INTERVAL")); return; } if (starts(s, "TIMEOUT=")) { cfg.toutSec = (uint16_t)atoi(s + 8); saveEEPROM(); Serial.println(F("OK TIMEOUT")); return; } if (starts(s, "FILE_MAX_SIZE=")) { cfg.maxBytes = (uint32_t)atol(s + 14); saveEEPROM(); Serial.println(F("OK FILE_MAX_SIZE")); return; } if (starts(s, "CAPTEUR=")) { char w[4]; char val[4]; if (sscanf(s + 8, "%3[^:]:%3s", w, val) == 2) { bool on = (val[0] == 'o' || val[0] == 'O'); if (w[0] == 'B' || w[0] == 'b') { enBME = on; } else if (w[0] == 'L' || w[0] == 'l') { enLUM = on; } saveEEPROM(); Serial.println(F("OK CAPTEUR")); } return; } if (starts(s, "DATE=")) { if (!haveRTC) { Serial.println(F("ERR RTC")); return; } int y, mo, d, h, mi, se; if (sscanf( s + 5, "%4d-%2d-%2d %2d:%2d:%2d", &y, &mo, &d, &h, &mi, &se ) == 6) { rtc.adjust(DateTime(y, mo, d, h, mi, se)); Serial.println(F("OK DATE")); } else { Serial.println(F("ERR DATE")); } return; } if (strcmp(s, "READ") == 0) { logOnce(); return; } if (strcmp(s, "EJECT") == 0) { if (logFile) { logFile.flush(); logFile.close(); } Serial.println(F("OK EJECT")); return; } if (strcmp(s, "RESET") == 0) { setDefaults(); saveEEPROM(); Serial.println(F("OK RESET")); return; } Serial.println(F("Commande inconnue. HELP")); } /* ---------- Alertes LED ---------- */ // priorité: SD_RW > SD_FULL > RTC > SENSOR > INCOH void updateErrorLED() { if (gErrors == ERR_NONE) { static LedColor last = LED_OFF; if (last != baseColor) { setLed(baseColor); last = baseColor; } return; } uint8_t e = (gErrors & ERR_SD_RW) ? ERR_SD_RW : (gErrors & ERR_SD_FULL) ? ERR_SD_FULL : (gErrors & ERR_RTC) ? ERR_RTC : (gErrors & ERR_SENSOR) ? ERR_SENSOR : ERR_INCOH; unsigned long now = millis(); uint16_t tS = 220; uint16_t tL = 600; uint16_t tE = 360; LedColor colA = LED_R; LedColor colB = LED_W; uint16_t halfA = tE; uint16_t halfB = tE; if (e == ERR_RTC) { colB = LED_B; halfA = halfB = tE; // 🔴↔🔵 égal } else if (e == ERR_SENSOR) { colB = LED_G; halfA = halfB = tE; // 🔴↔🟢 égal } else if (e == ERR_INCOH) { colB = LED_G; halfA = tS; halfB = tL; // 🔴 court / 🟢 long } else { colB = LED_W; halfA = (e == ERR_SD_RW) ? tS : tE; halfB = (e == ERR_SD_RW) ? tL : tE; // SD: rouge↔blanc } static uint8_t lastE = 0xFF; static uint16_t cur = 0; if (e != lastE) { lastE = e; ledPhase = false; ledBeatMs = now; cur = halfA; setLed(colA); return; } if (now - ledBeatMs >= cur) { ledBeatMs = now; ledPhase = !ledPhase; if (ledPhase) { setLed(colB); cur = halfB; } else { setLed(colA); cur = halfA; } } } /* ---------- Setup / Loop ---------- */ unsigned long lastCmdMs = 0; const unsigned long CFG_BACK = 30000UL; void setup() { Serial.begin(9600); led.init(); setLed(LED_W); pinMode(PIN_BTN_V, INPUT_PULLUP); pinMode(PIN_BTN_R, INPUT_PULLUP); attachInterrupt( digitalPinToInterrupt(PIN_BTN_V), isrV, CHANGE ); attachInterrupt( digitalPinToInterrupt(PIN_BTN_R), isrR, CHANGE ); loadEEPROM(); Wire.begin(); haveRTC = rtc.begin(); if (!haveRTC) { #if PRETTY_UI Serial.println(F("⚠️ RTC non détectée")); #else Serial.println(F("RTC abs")); #endif gErrors |= ERR_RTC; } if (SD.begin(PIN_SD_CS)) { haveSD = true; openNewLog(); } else { haveSD = false; #if PRETTY_UI Serial.println(F("⚠️ Carte SD absente")); #else Serial.println(F("SD abs")); #endif gErrors |= ERR_SD_RW; } for (uint8_t i = 0; i < sizeof(sInit) / sizeof(sInit[0]); i++) { if (sInit[i]->initFn) { sInit[i]->initFn(sInit[i]); } } if (!sBME.present && enBME) { gErrors |= ERR_SENSOR; } if (digitalRead(PIN_BTN_R) == LOW) { enterMode(CONFIGURATION); } else { enterMode(STANDARD); } #if PRETTY_UI Serial.println(F("✦ Aide: tapez HELP (Moniteur série 9600 bauds)")); #else Serial.println(F("HELP pr cmds")); #endif } void loop() { if (fEco) { fEco = false; enterMode(ECONOMIQUE); } if (fMaint) { fMaint = false; modePrev= modeCur; enterMode(MAINTENANCE); } if (fStd) { fStd = false; enterMode(STANDARD); } if (fQuitM) { fQuitM = false; enterMode(modePrev); } while (Serial.available()) { char c = (char)Serial.read(); handleCmdChar(c); lastCmdMs = millis(); } switch (modeCur) { case STANDARD: tickStd(); break; case ECONOMIQUE: tickEco(); break; case MAINTENANCE: tickMaint(); break; case CONFIGURATION: tickCfg(); break; } if (modeCur == CONFIGURATION && (millis() - lastCmdMs > CFG_BACK)) { enterMode(STANDARD); } updateErrorLED(); delay(3); }

Explication détaillée du code – Station météo embarquée

🎯Les bibliothèques utilisées dans le projet

Dans cette première partie, on explique ce qu’on a importé dans le code et pourquoi.

  1. #define PRETTY_UI 1

    • Ce n’est pas une bibliothèque, c’est un réglage.
    • Quand il vaut 1 → on affiche dans le moniteur série avec des emojis et des phrases jolies.
    • Quand on le met à 0 → on réduit le texte pour gagner de la mémoire sur l’Arduino UNO.
  2. #include <Wire.h>

    • Sert à utiliser le bus I2C.
    • Dans notre projet, l’I2C sert à parler avec l’horloge RTC DS1307 et aussi avec certains capteurs (BME280 peut être en I2C).
    • Sans cette bibliothèque, on ne peut pas faire Wire.begin() et donc pas lire l’heure.
  3. #include <SPI.h>

    • Sert à utiliser le bus SPI.
    • Dans notre projet, le SPI est surtout utilisé pour la carte SD.
    • C’est la couche de base sur laquelle la bibliothèque SD travaille.
  4. #include <SD.h>

    • C’est la bibliothèque qui permet d’ouvrir, écrire, fermer des fichiers sur la carte SD.
    • On l’utilise pour créer les fichiers LOG001.CSV, pour écrire les mesures dedans, et pour vérifier si la carte est présente.
  5. #include <EEPROM.h>

    • Permet de sauvegarder des paramètres dans la mémoire interne de l’Arduino (qui reste même quand on coupe l’alimentation).
    • On l’utilise pour ne pas perdre : l’intervalle de mesure, la taille max du fichier, quels capteurs sont activés.
    • Grâce à ça, quand on rallume la carte, elle garde la configuration.
  6. #include <RTClib.h>

    • Bibliothèque prête à l’emploi pour les modules d’horloge DS1307.
    • Elle nous donne des fonctions simples comme rtc.begin() pour tester le module, et rtc.now() pour récupérer date + heure.
    • On en a besoin parce que toutes les lignes qu’on enregistre doivent être horodatées (exigence du projet).
  7. #include <Adafruit_BME280.h>

    • Bibliothèque du capteur météo BME280.
    • Elle nous donne directement readTemperature(), readHumidity(), readPressure().
    • Sans elle, on serait obligé d’écrire tout le protocole du capteur à la main.
  8. #include <ChainableLED.h>

    • Bibliothèque pour piloter la LED RGB chainable.
    • Dans le projet, la LED sert à indiquer le mode et les erreurs.
    • Cette bibliothèque simplifie : au lieu de calculer le PWM pour chaque couleur, on appelle juste led.setColorRGB(...).

🎯 Explication complète des modes de fonctionnement

Le code comporte quatre modes de fonctionnement, exactement comme le cahier des charges du projet : Standard, Économique, Maintenance et Configuration.
Ces modes sont définis par l’énumération :

enum Mode { STANDARD, ECONOMIQUE, MAINTENANCE, CONFIGURATION };

Chaque mode a ses propres fonctions, son comportement et une couleur LED dédiée.


🔹 Mode Standard

Rôle

C’est le mode principal de fonctionnement du système. Il réalise les mesures régulières des capteurs et enregistre les données sur la carte SD.

Comment il fonctionne
Où c’est écrit dans le code

🔹 Mode Économique

Rôle

Ce mode est utilisé pour réduire la consommation d’énergie. Il permet au système de fonctionner plus longtemps sur batterie.

Comment il fonctionne
Où c’est écrit dans le code

🔹 Mode Maintenance

Rôle

Le mode Maintenance sert à consulter les données des capteurs sans les enregistrer et à retirer la carte SD en toute sécurité.

Comment il fonctionne
Où c’est écrit dans le code

🔹 Mode Configuration

Rôle

Ce mode permet à l’utilisateur de modifier les paramètres du système via la console série (moniteur Arduino). C’est le seul mode où les capteurs ne font aucune mesure.

Comment il fonctionne
Où c’est écrit dans le code

🔹 Récapitulatif des couleurs LED et actions

Mode Couleur LED Fonction principale Méthode d’activation
Standard 🟢 Vert Mesure et enregistrement réguliers Démarrage normal ou retour automatique
Économique 🔵 Bleu Mesure ralentie, économie d’énergie Bouton vert (2s)
Maintenance 🟠 Orange Lecture des capteurs sans écriture Bouton rouge (2s)
Configuration 🟡 Jaune Réglage des paramètres via série Bouton rouge au démarrage

🎯 Comment on a fait les interruptions (boutons)

Dans ce projet, les boutons ne sont pas lus simplement avec digitalRead() dans la boucle. On a choisi d’utiliser des interruptions matérielles. Ça permet au système de réagir tout de suite quand l’utilisateur appuie sur un bouton, même si le programme est en train d’écrire sur la carte SD ou de faire une mesure.

On va expliquer ça en 4 niveaux :

  1. Pourquoi on a utilisé des interruptions
  2. Comment on a relié les boutons au matériel
  3. Ce que font les fonctions d’interruption (isrV et isrR)
  4. Comment la boucle principale termine le travail

1. Pourquoi utiliser des interruptions ?

Sans interruption, on aurait fait :

if (digitalRead(bouton) == LOW) { ... }

mais ça veut dire qu’on doit tout le temps vérifier le bouton dans la boucle. Si la boucle est lente (écriture SD, calcul…), on peut rater un appui.

Avec les interruptions :

C’est exactement ce qu’on veut pour :


2. Connexion des boutons à des broches d’interruption

Dans le code, on utilise :

Ces deux broches (2 et 3) de l’Arduino UNO sont justement celles qui supportent les interruptions externes.

Dans le setup(), on voit :

pinMode(PIN_BTN_V, INPUT_PULLUP);
pinMode(PIN_BTN_R, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(PIN_BTN_V), isrV, CHANGE);
attachInterrupt(digitalPinToInterrupt(PIN_BTN_R), isrR, CHANGE);

👉 Ça veut dire :


3. Ce que fait une interruption dans ce projet

On a deux fonctions d’interruption :

void isrV(void) { ... }  // bouton vert
void isrR(void) { ... }  // bouton rouge

Très important : dans une interruption, on ne fait pas de choses longues (pas de Serial.print, pas d’accès SD). On fait juste le minimum : noter l’heure et poser un drapeau.

3.1. Détection de l’appui long

L’idée est la même pour les deux boutons :

  1. Quand le bouton passe à LOW → c’est qu’on vient d’appuyer → on enregistre l’instant avec millis().
  2. Quand le bouton repasse à HIGH → c’est qu’on a relâché → on regarde combien de temps il est resté appuyé.
  3. Si le temps ≥ 2 secondes → on considère que c’est une commande de changement de mode.

C’est pour ça qu’on a ces variables globales :

const unsigned long HOLD_MS = 2000; // 2s
volatile unsigned long tV = 0, tR = 0; // temps d’appui vert/rouge

Et dans l’ISR du bouton vert :

3.2. On ne change pas le mode ici

Dans l’ISR, on ne fait pas enterMode(...). À la place, on met juste un flag :

On utilise volatile bool pour ces flags parce qu’ils sont modifiés en dehors du flux normal (par l’interruption).


4. Qui s’occupe vraiment de changer de mode ?

C’est la boucle principale (la fonction loop()).

Dans loop(), on trouve :

if (fEco) { fEco = false; enterMode(ECONOMIQUE); }
if (fMaint) { fMaint = false; modePrev = modeCur; enterMode(MAINTENANCE); }
if (fStd) { fStd = false; enterMode(STANDARD); }
if (fQuitM) { fQuitM = false; enterMode(modePrev); }

Donc le fonctionnement complet est :

  1. Interruption → détecte l’appui long → met fEco = true (ou autre)
  2. loop() voit le flag → appelle enterMode(...)
  3. enterMode(...) change la LED, écrit dans le Serial, met à jour le mode courant
  4. ensuite le reste du code exécute le bon tick...() selon le mode

👉 C’est une bonne pratique en Arduino :


5. Lien avec le cahier des charges

Le sujet disait :

Donc toutes les actions “appui long → changer de mode” sont réalisées grâce aux interruptions.


Explication de TOUTES les fonctions utilisées

1. Fonctions de configuration générale

1.1 setDefaults()

But : remettre la configuration du système à des valeurs par défaut.
Ce qu’elle fait :

1.2 saveEEPROM()

But : enregistrer la configuration courante dans la mémoire EEPROM.
Ce qu’elle fait :

1.3 loadEEPROM()

But : lire la configuration stockée à l’allumage.
Ce qu’elle fait :

1.4 crc16(const Settings& s)

But : faire un petit contrôle d’intégrité sur la structure de config.
Ce qu’elle fait :


2. Fonctions liées aux LED

2.1 setLed(enum LedColor c)

But : allumer la LED avec la bonne couleur.
Ce qu’elle fait : traduit une couleur logique (LED_G, LED_B, LED_Y…) en valeurs RVB pour la bibliothèque ChainableLED.
Utilisée :

2.2 updateErrorLED()

But : afficher les erreurs système avec un code couleur/clignotement.
Ce qu’elle fait :


3. Fonctions de capteurs

3.1 initBME(struct Sensor* s)

But : initialiser le capteur BME280.
Ce qu’elle fait :

3.2 readBME(struct Sensor* s, float* t, float* h, float* p)

But : lire une mesure du BME.
Ce qu’elle fait :

3.3 initLUM(struct Sensor* s)

But : préparer la lecture de la luminosité sur l’entrée analogique.
Utilisée : dans le setup().

3.4 readLUM(struct Sensor* s, float* t, float* h, float* p)

But : lire la luminosité.
Ce qu’elle fait : lit analogRead(A0) et met la valeur dans p.
Utilisée : dans logOnce().


4. Fonctions d’affichage / impression

4.1 printDateTime(Print& out)

But : écrire la date et l’heure dans un flux (Serial ou fichier SD).
Ce qu’elle fait :

4.2 printNum(Print& out, float v, uint8_t prec)

But : afficher une valeur flottante proprement ou “NA”.
Utilisée : dans logOnce().

4.3 labelVal(...)

But : faire un affichage lisible pour l’utilisateur (Temp=25.4 °C).
Utilisée : dans la version “jolie” de l’affichage série (si PRETTY_UI = 1).


5. Fonctions de journalisation (SD)

5.1 openNewLog()

But : créer/ouvrir un nouveau fichier CSV sur la carte SD.
Ce qu’elle fait :

5.2 rotateIfNeed()

But : surveiller la taille du fichier courant et en recréer un nouveau si on dépasse la taille configurée.
Utilisée : à la fin de logOnce().

5.3 logOnce()

But : c’est la fonction centrale d’acquisition + stockage.
Ce qu’elle fait :

  1. lit tous les capteurs
  2. vérifie la cohérence des valeurs
  3. affiche les valeurs dans le moniteur série
  4. si la carte SD est là → écrit une ligne CSV
  5. met à jour les drapeaux d’erreur (SD, capteur, incohérence)
    Utilisée : par les modes Standard et Économique.

6. Fonctions liées aux modes

6.1 enterMode(enum Mode m)

But : basculer le système dans un des 4 modes.
Ce qu’elle fait :

6.2 tickStd()

But : comportement du mode Standard.
Ce qu’elle fait : appelle périodiquement logOnce() selon l’intervalle défini.

6.3 tickEco()

But : comportement du mode Économique.
Ce qu’elle fait : pareil que standard mais deux fois plus lent.

6.4 tickMaint()

But : comportement du mode Maintenance.
Ce qu’elle fait : ne fait rien (pas de log), laisse la main à l’utilisateur.

6.5 tickCfg()

But : comportement du mode Configuration.
Ce qu’elle fait : rien en boucle → on attend les commandes série.


7. Fonctions de commandes série (configuration)

7.1 handleCmdChar(char c)

But : recevoir les caractères un par un depuis le port série.
Ce qu’elle fait :

7.2 handleCmdLine()

But : analyser la ligne tapée par l’utilisateur et exécuter la commande.
Ce qu’elle fait :

7.3 showHelp()

But : afficher la liste des commandes disponibles.
Utilisée : quand l’utilisateur tape HELP.


8. Fonctions d’interruption

8.1 isrV() (bouton vert)

But : détecter un appui long sur le bouton vert.
Ce qu’elle fait :

8.2 isrR() (bouton rouge)

But : détecter un appui long sur le bouton rouge.
Ce qu’elle fait :


9. Fonctions principales Arduino

9.1 setup()

But : initialiser tout le système au démarrage.
Ce qu’elle fait :

9.2 loop()

But : faire tourner le système en continu.
Ce qu’elle fait :


Conclusion générale du projet

Notre équipe a réalisé une station météo embarquée complète et autonome à base d’Arduino UNO, capable de mesurer, enregistrer et signaler les données météorologiques de manière fiable. Le système gère quatre modes de fonctionnement, une LED d’état, une interface série pour la configuration et une mémoire EEPROM pour sauvegarder les paramètres. Ce projet nous a permis de comprendre concrètement le fonctionnement d’un système embarqué, de maîtriser les capteurs et la gestion de données, tout en respectant les exigences techniques du programme Worldwide Weather Watcher.